<template>
  <div ref="scrollbarRef" :class="ns.b()">
    <div ref="wrapRef" :class="wrapKls" :style="style" @scroll="handleScroll">
      <component
        :is="tag"
        ref="resizeRef"
        :class="resizeKls"
        :style="viewStyle">
        <slot />
      </component>
    </div>
    <template v-if="!native">
      <bar
        ref="barRef"
        :height="sizeHeight"
        :width="sizeWidth"
        :always="always"
        :ratio-x="ratioX"
        :ratio-y="ratioY" />
    </template>
  </div>
</template>
<script lang="ts" setup>
import { useEventListener, useResizeObserver } from '@vueuse/core'
import { isNumber, isObject } from 'lodash-es'
import type { CSSProperties, StyleValue } from 'vue'
import {
  computed,
  nextTick,
  onMounted,
  onUpdated,
  provide,
  reactive,
  ref,
  watch
} from 'vue'
import './scrollbar.less'

import type { BarInstance } from './bar'
import Bar from './bar.vue'
import { scrollbarContextKey } from './constants'
import { useNamespace } from './hooks'
import { scrollbarEmits } from './scrollbar'
import { addUnit, GAP } from './util'

const COMPONENT_NAME = 'VcScrollbar'

const props = withDefaults(
  defineProps<{
    /**
     * @description height of scrollbar
     */
    height?: string | number
    /**
     * @description max height of scrollbar
     */
    maxHeight?: string | number
    /**
     * @description whether to use the native scrollbar
     */
    native?: boolean
    /**
     * @description style of wrap
     */
    wrapStyle?: StyleValue
    /**
     * @description class of wrap
     */
    wrapClass?: string | string[]
    /**
     * @description class of view
     */
    viewClass?: string | string[]
    /**
     * @description style of view
     */
    viewStyle?: StyleValue
    /**
     * @description do not respond to container size changes, if the container size does not change, it is better to set it to optimize performance
     */
    noresize?: boolean // 如果 container 尺寸不会发生变化，最好设置它可以优化性能
    /**
     * @description element tag of the view
     */
    tag?: string
    /**
     * @description always show
     */
    always?: boolean
    /**
     * @description minimum size of scrollbar
     */
    minSize?: number
  }>(),
  {
    height: '',
    maxHeight: '',
    wrapStyle: '',
    wrapClass: '',
    viewClass: '',
    viewStyle: '',
    always: false,
    tag: 'div',
    minSize: 20
  }
)
const emit = defineEmits(scrollbarEmits)

const ns = useNamespace('scrollbar')

let stopResizeObserver: (() => void) | undefined = undefined
let stopResizeListener: (() => void) | undefined = undefined

const scrollbarRef = ref<HTMLDivElement>()
const wrapRef = ref<HTMLDivElement>()
const resizeRef = ref<HTMLElement>()

const sizeWidth = ref('0')
const sizeHeight = ref('0')
const barRef = ref<BarInstance>()
const ratioY = ref(1)
const ratioX = ref(1)

const style = computed<StyleValue>(() => {
  const style: CSSProperties = {}
  if (props.height) style.height = addUnit(props.height)
  if (props.maxHeight) style.maxHeight = addUnit(props.maxHeight)
  return [props.wrapStyle, style]
})

const wrapKls = computed(() => {
  return [
    props.wrapClass,
    ns.e('wrap'),
    { [ns.em('wrap', 'hidden-default')]: !props.native }
  ]
})

const resizeKls = computed(() => {
  return [ns.e('view'), props.viewClass]
})

const handleScroll = () => {
  if (wrapRef.value) {
    // @ts-ignore
    barRef.value?.handleScroll(wrapRef.value)

    emit('scroll', {
      scrollTop: wrapRef.value.scrollTop,
      scrollLeft: wrapRef.value.scrollLeft
    })
  }
}

// @ts-ignore
function scrollTo(xCord: number, yCord?: number): void
// @ts-ignore
// eslint-disable-next-line no-redeclare, no-undef
function scrollTo(options: ScrollToOptions): void
// eslint-disable-next-line no-redeclare
function scrollTo(arg1: unknown, arg2?: number) {
  if (isObject(arg1)) {
    // @ts-ignore
    wrapRef.value!.scrollTo(arg1)
  } else if (isNumber(arg1) && isNumber(arg2)) {
    // @ts-ignore
    wrapRef.value!.scrollTo(arg1, arg2)
  }
}

const setScrollTop = (value: number) => {
  if (!isNumber(value)) {
    throw new Error(COMPONENT_NAME + ' value must be a number')
  }
  wrapRef.value!.scrollTop = value
}

const setScrollLeft = (value: number) => {
  if (!isNumber(value)) {
    throw new Error(COMPONENT_NAME + ' value must be a number')
  }
  wrapRef.value!.scrollLeft = value
}

const update = () => {
  if (!wrapRef.value) return
  const offsetHeight = wrapRef.value.offsetHeight - GAP
  const offsetWidth = wrapRef.value.offsetWidth - GAP

  const originalHeight = offsetHeight ** 2 / wrapRef.value.scrollHeight
  const originalWidth = offsetWidth ** 2 / wrapRef.value.scrollWidth
  const height = Math.max(originalHeight, props.minSize)
  const width = Math.max(originalWidth, props.minSize)

  ratioY.value =
    originalHeight /
    (offsetHeight - originalHeight) /
    (height / (offsetHeight - height))
  ratioX.value =
    originalWidth /
    (offsetWidth - originalWidth) /
    (width / (offsetWidth - width))

  sizeHeight.value = height + GAP < offsetHeight ? `${height}px` : ''
  sizeWidth.value = width + GAP < offsetWidth ? `${width}px` : ''
}

watch(
  () => props.noresize,
  (noresize) => {
    if (noresize) {
      stopResizeObserver?.()
      stopResizeListener?.()
    } else {
      ;({ stop: stopResizeObserver } = useResizeObserver(resizeRef, update))
      stopResizeListener = useEventListener('resize', update)
    }
  },
  { immediate: true }
)

watch(
  () => [props.maxHeight, props.height],
  () => {
    if (!props.native)
      nextTick(() => {
        update()
        if (wrapRef.value) {
          // @ts-ignore
          barRef.value?.handleScroll(wrapRef.value)
        }
      })
  }
)

provide(
  scrollbarContextKey,
  // @ts-ignore
  reactive({
    scrollbarElement: scrollbarRef,
    wrapElement: wrapRef
  })
)

onMounted(() => {
  if (!props.native)
    nextTick(() => {
      update()
    })
})
onUpdated(() => update())

defineExpose({
  /** @description scrollbar wrap ref */
  wrapRef,
  /** @description update scrollbar state manually */
  update,
  /** @description scrolls to a particular set of coordinates */
  scrollTo,
  /** @description set distance to scroll top */
  setScrollTop,
  /** @description set distance to scroll left */
  setScrollLeft,
  /** @description handle scroll event */
  handleScroll
})
</script>
<style lang="less" scoped>
// @import url('./scrollbar.less');
</style>
